package ch.bfh.ti.barcode.multiple;

import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

import ch.bfh.ti.barcode.CodeGenerator;
import ch.bfh.ti.barcode.exception.EncodeException;

/**
 * This class is a wrapper for the CodeGenerator. It provides a way of splitting
 * data and sending it with multiple barcodes. Besides the data it can also send
 * a filename so a file being sent can be restored by the receiver. Every frame
 * being sent has an increasing index number. The total size of the data and the
 * total count of frames is being sent in the first frame. To send the images
 * with the barcodes, the CodeEventListener interface has to be implemented and
 * the implementation has to be attached with the method
 * {@link #addEventListener(CodeEventListener)}.
 * 
 * @author Dominik von Burg
 * 
 */
public class MultipleCodeGenerator {
	private final CodeGenerator generator;
	private final int frameDurationMs;
	private final int firstFrameDurationMs;
	private int frameNumber = 0;
	private long lastFrameTime;
	private final int totalFrames;
	private final String filename;
	private final int size;
	private List<CodeEventListener> listeners = new LinkedList<CodeEventListener>();
	private boolean errorCorrection;
	private int errorCorrectionFrequency;
	private Queue<Frame> errorCorrectionQueue;
	private boolean isDisposed;

	/**
	 * Creates a new MultipleCodeGenerator. The MultipleCodeGenerator should
	 * only be used to transfer data once. The size of the data and the filename
	 * have to be set at initialization an can not be changed later.
	 * 
	 * @param generator
	 *            An implementation of the interface CodeGenerator doing the
	 *            actual conversion of the data into a barcode.
	 * @param frameDurationMs
	 *            How long to wait between the generation of the barcode images.
	 *            If the generation of the image takes longer than this value
	 *            then it is being ignored.
	 * @param firstFrameDurationMs
	 *            Wait time between the first and second frame. Helps the
	 *            receiver prepare for receiving the frames.
	 * @param size
	 *            Total size in bytes of the data being sent with the method
	 *            {@link #sendData(byte[])}
	 * @param filename
	 *            If a file is being sent then the filename can be transmitted.
	 *            This value can also be empty.
	 */
	public MultipleCodeGenerator(CodeGenerator generator, int frameDurationMs,
			int firstFrameDurationMs, int size, String filename)
			throws EncodeException {
		this.generator = generator;
		this.frameDurationMs = frameDurationMs;
		this.firstFrameDurationMs = firstFrameDurationMs;
		this.filename = filename;
		
		if (generator.getCodeSize() <= Frame.getFirstFrameDataSize(0))
			throw new IllegalArgumentException(
					"The size of the frame is smaller than or equal to the header size!");
		
		// calculate how many frames we need
		this.size = size;
		size -= Frame.getFirstFrameDataSize(generator.getCodeSize());
		size = (int) Math.ceil((float) size
				/ (float) Frame.getSimpleFrameDataSize(generator.getCodeSize()));
		this.totalFrames = size + 1;

		lastFrameTime = System.currentTimeMillis();
	}

	public MultipleCodeGenerator(CodeGenerator generator, int frameDurationMs,
			int firstFrameDurationMs, int size, String filename,
			int errorCorrectionFrequency) throws EncodeException {
		this(generator, frameDurationMs, firstFrameDurationMs, size, filename);

		errorCorrection = true;
		this.errorCorrectionFrequency = errorCorrectionFrequency;
		errorCorrectionQueue = new LinkedList<Frame>();
	}

	/**
	 * The amount of data that can be sent can differ from frame to frame. It
	 * depends on the size of the barcode and the size of the header. The header
	 * of the first frame is bigger because the filename, the total frame count
	 * and the size have to be transmitted. This method returns the data size of
	 * the current frame.
	 * 
	 * @return The size of the data sent in a single barcode.
	 */
	public int getCodeSize() {
		if (frameNumber == 0)
			return Frame.getFirstFrameDataSize(generator.getCodeSize());
		else
			return Frame.getSimpleFrameDataSize(generator.getCodeSize());
	}

	/**
	 * Attaches a class implementing the CodeEventListener interface to this
	 * MultipleCodeGenerator. Every time a new barcode is being generated, the
	 * method defined in the interface is called on the attached class. The
	 * attached class can then process the image data.
	 * 
	 * @param listener
	 *            A class implementing the CodeEventListener interface.
	 */
	public void addEventListener(CodeEventListener listener) {
		listeners.add(listener);
	}

	/**
	 * Removes a attached class previously attached with the
	 * {@link #addEventListener(CodeEventListener)} method.
	 * 
	 * @param listener
	 *            A class implementing the CodeEventListener interface.
	 */
	public void removeEventListener(CodeEventListener listener) {
		listeners.remove(listener);
	}

	/**
	 * This method actually sends the data and calls the
	 * {@link CodeEventListener#codeSent(CodeEvent)} method in the listening
	 * classes. The method blocks until all the data is being sent.
	 * 
	 * @param data
	 *            The binary data which will be converted to barcodes.
	 * @throws EncodeException
	 *             Any errors happening while generating the barcodes. Consult
	 *             the actual implementation of the CodeGenerator for more
	 *             information.
	 */
	public void sendData(byte[] data) throws EncodeException {
		if(isDisposed)
			throw new EncodeException("Object is disposed!");
		
		Frame frame;

		if (frameNumber == 0)
			frame = new HeaderFrame(data, size, totalFrames, filename, errorCorrectionFrequency);
		else
			frame = new SimpleFrame(data, frameNumber);

		/*
		 * If the error correction is enabled, put the frames into a queue
		 * first. If the queue is full, calculate the correction frame and send
		 * the whole queue.
		 */
		if (errorCorrection) {
			// first frame is sent without error correction
			if (frameNumber == 0)
				processData(frame);
			else
				errorCorrectionQueue.add(new SimpleFrame(data.clone(), frameNumber));
			
			if (errorCorrectionQueue.size() >= errorCorrectionFrequency)
				sendWithErrorCorrection();
		} else {
			processData(frame);
		}
		frameNumber++;
	}

	private void sendWithErrorCorrection() throws EncodeException {
		// xor the correction frame
		Frame[] corrected = Frame.createCorrection(errorCorrectionQueue.toArray(new Frame[errorCorrectionQueue.size()]));
		
		// send error correction frames
		for(Frame f : corrected)
			processData(f);

		errorCorrectionQueue.clear();
		
		frameNumber++;
	}

	public void flush() throws EncodeException {
		if(isDisposed)
			throw new EncodeException("Object is disposed!");
		
		if (errorCorrection && errorCorrectionQueue.size() > 0)
			sendWithErrorCorrection();
	}

	private void processData(Frame frame) throws EncodeException {
		if(isDisposed)
			throw new EncodeException("Object is disposed!");
		
		int[] code = generator.generate(frame.getFrameData());

		int time = frameNumber == 0 ? firstFrameDurationMs : frameDurationMs;

		for (CodeEventListener listener : listeners)
			listener.codeSent(new CodeEvent(this, code));

		// wait until time is over
		long currentTime = System.currentTimeMillis();
		if (lastFrameTime + time > currentTime) {
			try {
				Thread.sleep((lastFrameTime + time) - currentTime);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
		lastFrameTime = System.currentTimeMillis();
	}
	
	/**
	 * Disposes the object. The {@link MultipleCodeGenerator#sendData(byte[])} method throws
	 * an Exception instantly. The {@link MultipleCodeGenerator#flush()}
	 * also throws an Exception after calling this method.
	 */
	public void dispose() {
		isDisposed = true;
		listeners = new LinkedList<CodeEventListener>();
	}
}
